package com.linkgie.galaxyframework.binaryserialize.impl;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import com.linkgie.galaxyframework.binaryserialize.BinaryCodec;
import com.linkgie.galaxyframework.binaryserialize.BinaryException;
import com.linkgie.galaxyframework.binaryserialize.BinaryType;
import com.linkgie.galaxyframework.binaryserialize.DataOptions;

import utils.io.BytesOutputBuffer;
import utils.io.BytesSlice;

public class BinaryTypeGenericRefConverter extends AbstractDynamicValueConverter {

	private final Object mutex = new Object();

	private BinaryCodecLookup encoderLookup;

	private Map<Class<?>, BinaryCodec> encoderCache;

	public BinaryTypeGenericRefConverter(Class<?> baseType, BinaryCodecLookup encoderLookup) {
		super(baseType);
		this.encoderLookup = encoderLookup;
		this.encoderCache = new ConcurrentHashMap<>();
	}

	private BinaryCodec lookupEncoder(Class<?> dataType) {
		BinaryCodec encoder = encoderCache.get(dataType);
		if (encoder != null) {
			return encoder;
		}
		synchronized (mutex) {
			encoder = encoderCache.get(dataType);
			if (encoder != null) {
				return encoder;
			}
			Class<?>[] intfs = dataType.getInterfaces();
			Class<?> binaryType = null;
			BinaryType anno = null;
			for (Class<?> itf : intfs) {
				anno = itf.getAnnotation(BinaryType.class);
				if (anno != null) {
					if (binaryType == null) {
						binaryType = itf;
					} else {
						throw new BinaryException(String.format(
								"Data object implements more than one interface annotated by @BinaryType! --[DataObject=%s]",
								dataType.toString()));
					}
				}
			}
			if (binaryType == null) {
				throw new BinaryException(
						String.format("Data object doesn't implement any interface annotated by @BinaryType! --[DataObject=%s]",
								dataType.toString()));
			}

			encoder = encoderLookup.lookup(binaryType);
			if (encoder == null) {
				throw new BinaryException(String.format(
						"The specified type hasn't been registered as a binary type! --[BinaryType=%s][DataObject=%s]",
						binaryType.toString(), dataType.toString()));
			}
			encoderCache.put(dataType, encoder);
		}

		return encoder;
	}

	@Override
	public int encodeDynamicValue(Object value, BytesOutputBuffer buffer, DataOptions options) {
		BinaryCodec binaryCodec = lookupEncoder(value.getClass());

		BytesOutputBuffer valueBuffer = new BytesOutputBuffer();
		int size = binaryCodec.encode(value, valueBuffer);

		size += writeSize(size, buffer);

		buffer.write(valueBuffer);
		return size;
	}

	@Override
	public Object decodeValue(BytesSlice dataSlice, DataOptions options) {
		int code = HeaderEncoder.resolveCode(dataSlice);
		long version = HeaderEncoder.resolveVersion(dataSlice);
		BinaryCodec binaryCodec = encoderLookup.lookup(code, version);
		if (binaryCodec == null) {
			throw new BinaryException(
					String.format("No data contract was registered with code[%s] and version[%s]!", code, version));
		}
		return binaryCodec.decode(dataSlice.getInputStream());
	}

}
